use std::net::{SocketAddr, TcpListener};
use std::time::Duration;

use bytes::Bytes;
use http_body_util::Full;
// use hyper::client::conn::http1::SendRequest;
use hyper::header::CONTENT_TYPE;
use hyper::{Method, Request, StatusCode};
use hyper_util::rt::tokio::TokioIo;
use rust_collector::command::{Command, CommandBroadcaster};
use rust_collector::command::service::CommandService;
use tokio::net::TcpStream;
use tokio::time::timeout;

fn pick_free_port() -> u16 {
    let l = TcpListener::bind("127.0.0.1:0").expect("bind temp");
    let port = l.local_addr().unwrap().port();
    drop(l);
    port
}

async fn http_post_json(addr: SocketAddr, path: &str, body: &str) -> StatusCode {
    let stream = TcpStream::connect(addr).await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
    tokio::spawn(async move { let _ = conn.await; });

    let req: Request<Full<Bytes>> = Request::builder()
        .method(Method::POST)
        .uri(format!("http://{}{}", addr, path))
        .header(CONTENT_TYPE, "application/json")
        .body(Full::new(Bytes::from(body.as_bytes().to_vec())))
        .unwrap();

    let resp = timeout(Duration::from_secs(2), sender.send_request(req)).await.unwrap().unwrap();
    resp.status()
}

#[tokio::test]
async fn linkup_and_linkdown_broadcast() {
    let broadcaster = std::sync::Arc::new(CommandBroadcaster::new());
    let mut rx = broadcaster.subscribe();
    let svc = CommandService::new(broadcaster.clone());
    let addr: SocketAddr = format!("127.0.0.1:{}", pick_free_port()).parse().unwrap();

    tokio::spawn(async move { let _ = svc.run(addr).await; });
    tokio::time::sleep(Duration::from_millis(100)).await;

    let st = http_post_json(addr, "/api/command", "{\"cmd\":\"LinkUp\"}").await;
    assert_eq!(st, StatusCode::OK);
    let cmd = timeout(Duration::from_secs(2), rx.recv()).await.unwrap().unwrap();
    assert!(matches!(cmd, Command::LinkUp));

    let st = http_post_json(addr, "/api/command", "{\"cmd\":\"LinkDown\"}").await;
    assert_eq!(st, StatusCode::OK);
    let cmd = timeout(Duration::from_secs(2), rx.recv()).await.unwrap().unwrap();
    assert!(matches!(cmd, Command::LinkDown));
}

#[tokio::test]
async fn bad_method_and_path_and_json() {
    let broadcaster = std::sync::Arc::new(CommandBroadcaster::new());
    let svc = CommandService::new(broadcaster.clone());
    let addr: SocketAddr = format!("127.0.0.1:{}", pick_free_port()).parse().unwrap();
    tokio::spawn(async move { let _ = svc.run(addr).await; });
    tokio::time::sleep(Duration::from_millis(100)).await;

    // wrong path
    let stream = TcpStream::connect(addr).await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
    tokio::spawn(async move { let _ = conn.await; });

    let req: Request<Full<Bytes>> = Request::builder()
        .method(Method::POST)
        .uri(format!("http://{}/wrong", addr))
        .header(CONTENT_TYPE, "application/json")
        .body(Full::new(Bytes::from_static(b"{}")))
        .unwrap();
    let resp = sender.send_request(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);

    // wrong method
    let stream = TcpStream::connect(addr).await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
    tokio::spawn(async move { let _ = conn.await; });
    let req: Request<Full<Bytes>> = Request::builder()
        .method(Method::GET)
        .uri(format!("http://{}/api/command", addr))
        .body(Full::new(Bytes::from_static(b"")))
        .unwrap();
    let resp = sender.send_request(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);

    // bad json
    let st = http_post_json(addr, "/api/command", "not-json").await;
    assert_eq!(st, StatusCode::BAD_REQUEST);

    // unknown command
    let st = http_post_json(addr, "/api/command", "{\"cmd\":\"unknown\"}").await;
    assert_eq!(st, StatusCode::BAD_REQUEST);
}


